package org.anno.chunkdb;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.CharStreams;
import com.google.inject.Singleton;

import au.com.bytecode.opencsv.CSVReader;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * Creates tables from uploaded CSV file.
 */
@Singleton
public class UploadServlet extends HttpServlet {

  private final TableDao dao;
  private final int maxChunkSize;

  @VisibleForTesting
  public UploadServlet(TableDao dao, int maxChunkSize) {
    this.dao = dao;
    this.maxChunkSize = maxChunkSize;
  }

  public UploadServlet() {
    this(new TableDao(), 1024 * 1024);
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse res)
      throws ServletException, IOException {
    try {
      res.sendRedirect("/table/" + saveRowsAndBuildTable(getItemIterator(req)));
    } catch (FileUploadException e) {
      throw new ServletException(e);
    }
  }

  @VisibleForTesting
  FileItemIterator getItemIterator(HttpServletRequest req) throws FileUploadException, IOException {
    return new ServletFileUpload().getItemIterator(req);
  }

  /**
   * Extracts items from multipart/form-data. Regular form fields are matched to table properties.
   * Sets table name from name of the file attachment. Parses contents as CSV. Extracts first line
   * as column names and builds chunk from subsequent lines. Allocates a key and saves the chunk.
   */
  @VisibleForTesting
  String saveRowsAndBuildTable(FileItemIterator fileItemIterator)
      throws FileUploadException, IOException {
    Metadata.Table.Builder tableBuilder = Metadata.Table.newBuilder();
    String tableId = null;
    char sep = ',';
    while (fileItemIterator.hasNext()) {
      FileItemStream item = fileItemIterator.next();
      if (item.isFormField()) {
        log("Got a form field: " + item.getFieldName());
        String v = CharStreams.toString(new InputStreamReader(item.openStream()));
        switch (item.getFieldName()) {
          case "description":
            tableBuilder.setDescription(v);
            break;
          case "attribution":
            tableBuilder.setAttribution(v);
            break;
          case "attributionLink":
            tableBuilder.setAttributionLink(v);
            break;
          case "sep":
            if (v.length() > 0) {
              sep = v.charAt(0);
            }
        }
      } else {
        log("Got an uploaded file: " + item.getFieldName() + ", name = " + item.getName());
        tableBuilder.setName(item.getName());
        try (CSVReader reader = new CSVReader(new InputStreamReader(item.openStream()), sep)) {
          String[] firstLine = reader.readNext();
          for (int i = 0; i < firstLine.length; i++) {
            tableBuilder.addColumnBuilder()
                .setColumnId(i)
                .setName(firstLine[i])
                .setType(Metadata.Column.Type.STRING);
          }
          tableId = dao.create(tableBuilder);
          List<Metadata.Column> columns = tableBuilder.buildPartial().getColumnList();
          ChunkBuilder chunkBuilder = new ChunkBuilder(columns);
          int emptySize = chunkBuilder.getSize();
          for (String[] line; (line = reader.readNext()) != null; ) {
            if (chunkBuilder.getSize() > emptySize
                && chunkBuilder.getEstimatedSizeAfterAdding(line) >= maxChunkSize) {
              dao.addChunk(tableId, chunkBuilder.build());
              chunkBuilder = new ChunkBuilder(columns);
            }
            chunkBuilder.addRow(Arrays.asList(line));
          }

          if (chunkBuilder.getSize() > emptySize) {
            dao.addChunk(tableId, chunkBuilder.build());
          }
        }
      }
    }
    return tableId;
  }
}
